package com.lemon.exam.service.impl;

import com.lemon.exam.common.constant.ApiConst;
import com.lemon.exam.common.constant.RedisKeyConst;
import com.lemon.exam.common.crud.BaseServiceImpl;
import com.lemon.exam.common.entity.system.SysWeb;
import com.lemon.exam.common.entity.vo.SelectVO;
import com.lemon.exam.common.entity.vo.TreeSelectVO;
import com.lemon.exam.common.enums.StatusEnum;
import com.lemon.exam.common.exception.CustomException;
import com.lemon.exam.common.redis.RedisService;
import com.lemon.exam.common.util.SecurityUtil;
import com.lemon.exam.entity.po.MenuPO;
import com.lemon.exam.entity.vo.MenuSelectVO;
import com.lemon.exam.entity.vo.MenuVO;
import com.lemon.exam.repository.MenuRepository;
import com.lemon.exam.service.IMenuService;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.reactivestreams.Publisher;
import org.springframework.data.relational.core.query.Criteria;
import org.springframework.data.relational.core.query.Query;
import org.springframework.data.relational.core.query.Update;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.core.scheduler.Schedulers;
import reactor.util.function.Tuple2;

import java.util.*;
import java.util.function.Function;
import java.util.stream.Collectors;

import static com.lemon.exam.common.constant.ApiConst.INSERT_SUCCESS;
import static com.lemon.exam.common.constant.ApiConst.UPDATE_SUCCESS;
import static com.lemon.exam.common.enums.CodeEnum.ERROR_402;

/**
 * 菜单表 服务实现类
 *
 * @author Lemon
 * @since 2025/04/04 14:23:21
 */
@Slf4j
@Service
public class MenuServiceImpl extends BaseServiceImpl<MenuPO, Long, MenuRepository> implements IMenuService {
    @Resource
    private RedisService redisService;

    @Override
    public Mono<?> findById(Long id) {
        return Mono.zip(
                repository.findById(id).subscribeOn(Schedulers.boundedElastic()),
                redisService.set().get(RedisKeyConst.SYSTEM_INTERFACE, SysWeb.class).collectMap(SysWeb::path, SysWeb::name).subscribeOn(Schedulers.boundedElastic())
        ).map(tuple -> {
            MenuPO menu = tuple.getT1();
            if (!StringUtils.hasText(menu.getName())) {
                menu.setName(tuple.getT2().get(menu.getPath()));
            }
            return menu;
        });
    }

    @Override
    public Flux<?> find(Map<String, Object> param) {
        return redisService.set().get(RedisKeyConst.SYSTEM_INTERFACE, SysWeb.class)
                .collectMap(SysWeb::path, SysWeb::name)
                .flatMapMany(map -> {
                    if (CollectionUtils.isEmpty(param) || (!StringUtils.hasText((String) param.get("name")) && null == param.get("status"))) {
                        return repository.findAllMenuVO().collectList().flatMapMany(buildMenu(map));
                    }
                    return this.findTreeLinkIdList(param, map).flatMapMany(toMenu(map));
                });
    }

    /**
     * 转换为树形菜单
     *
     * @param map
     * @return
     */
    private Function<List<String>, Publisher<? extends MenuVO>> toMenu(Map<String, String> map) {
        return treeLinkIdList -> {
            List<Long> idList = treeLinkIdList.stream()
                    .filter(Objects::nonNull)
                    .flatMap(link -> Arrays.stream(link.split(",")))
                    .map(String::trim)
                    .map(Long::parseLong)
                    .distinct()
                    .toList();
            if (idList.isEmpty()) {
                return Flux.empty();
            }
            return repository.findAllMenuByIdList(idList).collectList().flatMapMany(buildMenu(map));
        };
    }

    /**
     * 构建菜单
     *
     * @param map
     * @return
     */
    private static Function<List<MenuVO>, Publisher<? extends MenuVO>> buildMenu(Map<String, String> map) {
        return menuList -> {
            //1.设置菜单名
            menuList.forEach(menu -> {
                if (!StringUtils.hasText(menu.getName())) {
                    menu.setName(map.get(menu.getPath()));
                }
            });

            //2.按parentId分组
            Map<Long, List<MenuVO>> groupMenu = menuList.stream().collect(Collectors.groupingBy(MenuVO::getParentId));

            //3.构建子菜单
            List<MenuVO> allChildMenus = new ArrayList<>();
            menuList.forEach(menu -> {
                List<MenuVO> children = groupMenu.get(menu.getId());
                if (children != null) {
                    menu.setChildren(children);
                    allChildMenus.addAll(children);
                }
            });

            //4.返回顶级菜单
            return Flux.fromIterable(menuList.stream()
                    .filter(menu -> !allChildMenus.contains(menu))
                    .filter(menu -> ApiConst.SUPER_PARENT_ID.equals(menu.getParentId()))
                    .toList());
        };
    }


    /**
     * 根据name查询出所有符合条件的treeLinkId
     *
     * @param param
     * @param pathMap
     * @return
     */
    private Mono<List<String>> findTreeLinkIdList(Map<String, Object> param, Map<String, String> pathMap) {
        String name = (String) param.get("name");
        String status = (String) param.get("status");

        //1.构建查询条件
        Criteria criteria = Criteria.empty();

        if (StringUtils.hasText(name)) {
            // 获取匹配的path列表
            List<String> matchedPaths = pathMap.entrySet()
                    .stream()
                    .filter(entry -> entry.getValue().contains(name.trim()))
                    .map(Map.Entry::getKey)
                    .toList();

            Criteria nameCriteria = Criteria.where("name").like("%" + name + "%");

            if (!matchedPaths.isEmpty()) {
                criteria = criteria.and(nameCriteria.or(Criteria.where("path").in(matchedPaths)));
            } else {
                criteria = criteria.and(nameCriteria);
            }
        }

        if (StringUtils.hasText(status)) {
            criteria = criteria.and("status").is(status);
        }

        //2.执行查询
        return template.select(MenuPO.class)
                .matching(Query.query(criteria).columns("tree_link_id"))
                .all()
                .map(MenuPO::getTreeLinkId)
                .collectList()
                .subscribeOn(Schedulers.boundedElastic());
    }

    @Override
    public Mono<MenuSelectVO> findSelect() {
        return Mono.zip(
                this.findParentMenuSelect().collectList(),
                this.findPathSelect().collectList()
        ).map(tuple -> new MenuSelectVO(tuple.getT2(), tuple.getT1()));
    }

    /**
     * 查询父级菜单
     *
     * @return
     */
    private Flux<TreeSelectVO> findParentMenuSelect() {
        return repository.findAllMenuIdMenuName()
                .map(menu -> new TreeSelectVO(
                        menu.getId(),
                        menu.getName(),
                        StatusEnum.UNABLE.getValue().equals(menu.getStatus()),
                        menu.getParentId(),
                        null
                ))
                .collectList()
                .flatMapMany(TO_TREE_SELECT)
                .subscribeOn(Schedulers.boundedElastic());
    }

    /**
     * 转换为树形结构
     */
    private static final Function<List<TreeSelectVO>, Publisher<? extends TreeSelectVO>> TO_TREE_SELECT = menuList -> {
        //1.按parentId分组
        Map<Long, List<TreeSelectVO>> groupMenu = menuList.stream().collect(Collectors.groupingBy(TreeSelectVO::getParentId));

        //2.构建子菜单
        List<TreeSelectVO> allChildMenus = new ArrayList<>();
        menuList.forEach(menu -> {
            List<TreeSelectVO> children = groupMenu.get(menu.getValue());
            if (children != null) {
                menu.setChildren(children);
                allChildMenus.addAll(children);
            }
        });

        //3.所有菜单
        List<TreeSelectVO> allMenuList = menuList.stream()
                .filter(menu -> !allChildMenus.contains(menu))
                .filter(menu -> ApiConst.SUPER_PARENT_ID.equals(menu.getParentId()))
                .toList();

        //4.返回顶级惨淡
        return Flux.just(new TreeSelectVO(ApiConst.SUPER_PARENT_ID, ApiConst.SUPER_MENU_NAME, allMenuList));
    };

    /**
     * 查询路径选择
     *
     * @return
     */
    private Flux<SelectVO> findPathSelect() {
        return Mono.zip(
                redisService.set().get(RedisKeyConst.SYSTEM_INTERFACE, SysWeb.class).map(SysWeb::path).collectList().subscribeOn(Schedulers.boundedElastic()),
                repository.findAllPath().map(MenuPO::getPath).collectList().subscribeOn(Schedulers.boundedElastic())
        ).flatMapMany(tuple -> Flux.fromIterable(tuple.getT1()).map(path -> new SelectVO(path, path, tuple.getT2().contains(path)))).subscribeOn(Schedulers.boundedElastic());
    }

    @Override
    public Mono<String> insert(MenuPO param) {
        return validateParams(param)
                .then(checkDuplicateName(param))
                .then(Mono.defer(() -> insertMenu(param)))
                .as(transactionalOperator::transactional);
    }

    /**
     * 校验参数
     *
     * @param param
     * @return
     */
    private Mono<Void> validateParams(MenuPO param) {
        if (!StringUtils.hasText(param.getName()) && !StringUtils.hasText(param.getPath())) {
            return Mono.error(new CustomException(402, "菜单名称或路径不能同时为空"));
        }
        return Mono.empty();
    }

    /**
     * 检查名称重复
     *
     * @param param
     * @return
     */
    private Mono<Void> checkDuplicateName(MenuPO param) {
        return Mono.zip(
                redisService.set().get(RedisKeyConst.SYSTEM_INTERFACE, SysWeb.class).collectMap(SysWeb::path, SysWeb::name).subscribeOn(Schedulers.boundedElastic()),
                repository.findAllByParentId(param.getParentId()).collectList().subscribeOn(Schedulers.boundedElastic())
        ).flatMap(duplicateName(param));
    }

    /**
     * 插入菜单
     *
     * @param menu
     * @return
     */
    private Mono<String> insertMenu(MenuPO menu) {
        return Mono.zip(this.getSeq(), SecurityUtil.getUsername())
                .flatMap(tuple -> {
                    menu.setSeq(tuple.getT1());
                    menu.setCreateBy(tuple.getT2());
                    return template.insert(menu).flatMap(savedMenu -> {
                        if (ApiConst.SUPER_PARENT_ID.equals(menu.getParentId())) {
                            return setTreeLinkForRootMenu(savedMenu);
                        } else {
                            return setTreeLinkForChildMenu(savedMenu);
                        }
                    });
                });
    }

    /**
     * 设置树链路
     *
     * @param menu
     * @return
     */
    private Mono<String> setTreeLinkForRootMenu(MenuPO menu) {
        return updateTreeLinkId(menu.getId(), "%d,%d".formatted(ApiConst.SUPER_PARENT_ID, menu.getId()));
    }

    /**
     * 设置树链路
     *
     * @param menu
     * @return
     */
    private Mono<String> setTreeLinkForChildMenu(MenuPO menu) {
        return repository.findTreeLinkIdById(menu.getParentId())
                .flatMap(parentMenu -> updateTreeLinkId(menu.getId(), "%s,%d".formatted(parentMenu.getTreeLinkId(), menu.getId())));
    }

    /**
     * 更新树链路
     *
     * @param menuId
     * @param treeLinkId
     * @return
     */
    private Mono<String> updateTreeLinkId(Long menuId, String treeLinkId) {
        return template.update(MenuPO.class)
                .matching(Query.query(Criteria.where("id").is(menuId)))
                .apply(Update.update("tree_link_id", treeLinkId))
                .thenReturn(INSERT_SUCCESS);
    }

    /**
     * 获取序列
     *
     * @return
     */
    private Mono<Integer> getSeq() {
        return databaseClient.sql("SELECT MAX(seq + 1) max_seq FROM sys_menu")
                .map(row -> row.get("max_seq", Integer.class))
                .one()
                .subscribeOn(Schedulers.boundedElastic());
    }

    @Override
    public Mono<String> update(MenuPO param) {
        return validateParams(param)
                .then(checkDuplicateNameByUpdate(param))
                .then(Mono.defer(() -> updateMenu(param)))
                .as(transactionalOperator::transactional);
    }

    /**
     * 检查名称重复
     *
     * @param param
     * @return
     */
    private Mono<Void> checkDuplicateNameByUpdate(MenuPO param) {
        return Mono.zip(
                redisService.set().get(RedisKeyConst.SYSTEM_INTERFACE, SysWeb.class).collectMap(SysWeb::path, SysWeb::name).subscribeOn(Schedulers.boundedElastic()),
                repository.findAllByIdAndParentId(param.getId(), param.getParentId()).collectList().subscribeOn(Schedulers.boundedElastic())
        ).flatMap(duplicateName(param));
    }

    /**
     * 检查重复名称
     *
     * @param param
     * @return
     */
    private static Function<Tuple2<Map<String, String>, List<MenuPO>>, Mono<? extends Void>> duplicateName(MenuPO param) {
        return tuple -> tuple.getT2().stream().peek(menu -> {
                    if (!StringUtils.hasText(menu.getName())) {
                        menu.setName(tuple.getT1().get(menu.getPath()));
                    }
                })
                .anyMatch(menu -> menu.getName().equals(param.getName())) ? Mono.error(new CustomException(402, "同级别名称重复")) : Mono.empty();
    }

    /**
     * 更新菜单
     *
     * @param menu
     * @return
     */
    private Mono<String> updateMenu(MenuPO menu) {
        return Mono.defer(() -> {
            if (ApiConst.SUPER_PARENT_ID.equals(menu.getParentId())) {
                return SecurityUtil.getUsername()
                        .flatMap(username -> doUpdateMenu(menu, username, "%d,%d".formatted(ApiConst.SUPER_PARENT_ID, menu.getId())));
            }
            return Mono.zip(
                    SecurityUtil.getUsername(),
                    repository.findTreeLinkIdById(menu.getParentId())
            ).flatMap(tuple -> doUpdateMenu(menu, tuple.getT1(), "%s,%d".formatted(tuple.getT2().getTreeLinkId(), menu.getId())));
        });
    }

    /**
     * 执行修改
     *
     * @param menu
     * @param username
     * @param treeLinkId
     * @return
     */
    private Mono<String> doUpdateMenu(MenuPO menu, String username, String treeLinkId) {
        return template.update(MenuPO.class)
                .matching(Query.query(Criteria.where("id").is(menu.getId())))
                .apply(Update.update("name", menu.getName())
                        .set("path", menu.getPath())
                        .set("parent_id", menu.getParentId())
                        .set("icon", menu.getIcon())
                        .set("tree_link_id", treeLinkId)
                        .set("update_by", username))
                .thenReturn(UPDATE_SUCCESS)
                .onErrorResume(err -> Mono.error(new CustomException(ERROR_402)));
    }

    @Override
    public Flux<MenuVO> findAllByStatus() {
        return repository.findAllByStatus();
    }
}
